package com.smartalarmclock.Model;

import java.util.Hashtable;
import java.util.Map.Entry;
import java.util.Set;

import com.smartalarmclock.Model.Exceptions.InvalidDeleteDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.InvalidSaveDataEntityOperationException;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

public abstract class DataEntity implements IDataEntity {
	
	// Database Constants
	
	public static final String c_ColumnNameId = "id";
	
	// Members
	
	private static final int c_UnsavedInstanceId = -1;
	private int m_Id;
	private boolean m_IsUpdated;
	
	// Protected Constructors
	
	/**
	 * Protected Constructor for new instances
	 */
	protected DataEntity() {
		m_Id = c_UnsavedInstanceId;
		m_IsUpdated = true;
	}
	
	/**
	 * Protected Constructor for instances loaded from the DB.
	 * @param id the existing id of the instance in the DB.
	 */
	protected DataEntity(int id) {
		m_Id = id;
		m_IsUpdated = false;
	}
	
	// IDataEntity implementation
	
	/**
	 * @return the instance id
	 */
	public int GetId(){
		return m_Id;
	}
	
	/**
	 * Saves the instance in the database.
	 * This method should be overriden in concrete classes for value validations
	 * and this method should be called for the actual save operation.
	 * @param context the context of the request (the originating Activity)
	 * @throws InvalidSaveDataEntityOperationException if values are invalid
	 */
	public void Save(Context context) throws InvalidSaveDataEntityOperationException {
		if (IsNewInstance() || IsUpdated()) {
			SQLiteDatabase db = OpenDBForWriting(context);
			
			String tableName = null;
			ContentValues values = null;
			int idToUse = m_Id;
			
			for (Entry<String, ContentValues> entry : GetRecordValues().entrySet()) {
				tableName = entry.getKey();
				values = entry.getValue();
				
				if (!IsNewInstance()) {
					// Updating existing record in the table
					db.update(tableName, values, c_ColumnNameId + "=" + m_Id, null);
				}
				else {
					// if the id was not created yet in previous tables, get a fresh id
					int newId = idToUse != c_UnsavedInstanceId ? idToUse : GetNextAvailableId(db, tableName);
					values.put(c_ColumnNameId, newId);
					
					// Inserting a new record into the table with a new unique id.
					db.insert(tableName, null, values);
					
					// Updating the id only after saving the instance
					idToUse = newId;
				}
			}
			
			db.close();
			
			// Updating id if it was created
			m_Id = idToUse;
			m_IsUpdated = false;
		}
	}
	
	/**
	 * Deletes the instance from the database
	 * @param context the context of the request (the originating Activity)
	 * @throws InvalidDeleteDataEntityOperationException if the data entity is referenced and cannot be deleted
	 * @throws InvalidSaveDataEntityOperationException if the save operation of the referencing data entity was invalid.
	 */
	public void Delete(Context context) throws InvalidDeleteDataEntityOperationException, InvalidSaveDataEntityOperationException {
		SQLiteDatabase db = OpenDBForWriting(context);
		
		// If the instance is marked as new, it wasn't saved in the DB yet,
		// so we don't need to delete it.
		if (!IsNewInstance()) {
			for (String tableName : GetTableNames()) {
				db.delete(tableName, c_ColumnNameId + "=" + m_Id, null);
				m_Id = c_UnsavedInstanceId;
			}
		}
		
		db.close();
	}
	
	// Comparable implementation
	
	@Override
	public int compareTo(IDataEntity another) {
		return toString().compareTo(another.toString());
	}
	
	// Overriding Methods
	
	@Override
	public boolean equals(Object o) {
		if (this.getClass() != o.getClass()) {
			return false;
		}
		
		return compareTo((IDataEntity)o) == 0;
	}
	
	@Override
	public int hashCode() {
		return toString().hashCode();
	}
	
	@Override
	public String toString() {
		return getClass().getSimpleName().concat(String.valueOf(GetId()));
	}
	
	// Protected Methods
	
	/**
	 * @return whether the instance is new (not saved in the DB yet) or not
	 */
	protected boolean IsNewInstance() {
		return m_Id == c_UnsavedInstanceId;
	}
	
	/**
	 * @return whether the instance was updated or not
	 */
	protected boolean IsUpdated() {
		return m_IsUpdated;
	}
	
	/**
	 * Sets the instance as updated so the instance will be saved to the DB
	 * when asked.
	 */
	protected void SetUpdated() {
		m_IsUpdated = true;
	}
	
	/**
	 * Returns a writable instance of the DB.
	 * @param context the context of the request (the originating Activity)
	 * @return a writable instance of the database
	 */
	protected static SQLiteDatabase OpenDBForWriting(Context context) {
		SmartAlarmClockDBHelper dbHelper = new SmartAlarmClockDBHelper(context);
		return dbHelper.getWritableDatabase();
	}
	
	/**
	 * Returns a readable instance of the DB. Updating, inserting and deleting
	 * is not permitted using this instance of the DB.
	 * @param context the context of the request (the originating Activity)
	 * @return a readable instance of the database
	 */
	protected static SQLiteDatabase OpenDBForReading(Context context) {
		SmartAlarmClockDBHelper dbHelper = new SmartAlarmClockDBHelper(context);
		return dbHelper.getReadableDatabase();
	}
		
	// Private Methods
	
	/**
	 * Queries a table for the next available id
	 * @param db an instance of the DB.
	 * @param tableName the name of the table to query
	 * @return the next available id in the given table
	 */
	private int GetNextAvailableId(SQLiteDatabase db, String tableName) {
		// querying the table for the maximum id
		Cursor cursor = db.rawQuery(
			"SELECT MAX(" + c_ColumnNameId + ")" +
			"FROM " + tableName,
			null);
		
		// Fist id in the table is 1
		int lastId = 0;
		if (cursor.moveToNext()) {
			lastId = cursor.getInt(0);
		}
		
		cursor.close();
		return lastId + 1;
	}
	
	// Abstract Methods For Deriving Classes
	
	/**
	 * @return the ContentValues containing the values of the object for the DB for each table name
	 */
	protected abstract Hashtable<String, ContentValues> GetRecordValues();
	
	/**
	 * @return all the table names effected by the entity
	 */
	protected abstract Set<String> GetTableNames();
}
